#!/usr/bin/env python
#
# Copyright 2006,2007,2011,2012,2019,2020 Free Software Foundation, Inc.
#
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
#
"""
This example application demonstrates receiving and demodulating
different types of signals using the USRP.

A receive chain is built up of the following signal processing
blocks:

USRP  - Daughter board source generating complex baseband signal.
CHAN  - Low pass filter to select channel bandwidth
RFSQL - RF squelch zeroing output when input power below threshold
AGC   - Automatic gain control leveling signal at [-1.0, +1.0]
DEMOD - Demodulation block appropriate to selected signal type.
        This converts the complex baseband to real audio frequencies,
        and applies an appropriate low pass decimating filter.
CTCSS - Optional tone squelch zeroing output when tone is not present.
RSAMP - Resampler block to convert audio sample rate to user specified
        sound card output rate.
AUDIO - Audio sink for playing final output to speakers.

The following are required command line parameters:

-f FREQ         USRP receive frequency
-m MOD          Modulation type, select from AM, FM, or WFM

The following are optional command line parameters:

-R SUBDEV       Daughter board specification, defaults to first found
-c FREQ         Calibration offset.  Gets added to receive frequency.
                Defaults to 0.0 Hz.
-g GAIN         Daughterboard gain setting. Defaults to mid-range.
-o RATE         Sound card output rate. Defaults to 32000. Useful if
                your sound card only accepts particular sample rates.
-r RFSQL        RF squelch in db. Defaults to -50.0.
-p FREQ         CTCSS frequency.  Opens squelch when tone is present.

Once the program is running, ctrl-break (Ctrl-C) stops operation.

Please see fm_demod.py and am_demod.py for details of the demodulation
blocks.
"""

import math
import sys
from argparse import ArgumentParser
from gnuradio import gr, uhd, audio
from gnuradio import filter
from gnuradio import analog
from gnuradio import eng_notation, eng_arg
from gnuradio.eng_option import eng_option


# (device_rate, channel_rate, audio_rate, channel_pass, channel_stop, demod)
DEMOD_PARAMS = {
    'AM': (256000, 16000, 16000, 5000, 8000, analog.demod_10k0a3e_cf),
    'FM': (256000, 32000, 8000, 8000, 9000, analog.demod_20k0f3e_cf),
    'WFM': (320000, 320000, 32000, 80000, 115000, analog.demod_200kf3e_cf)
}


class uhd_src(gr.hier_block2):
    """
    Create a UHD source object supplying complex floats.

    Selects user supplied subdevice or chooses first available one.

    Calibration value is the offset from the tuned frequency to
    the actual frequency.
    """

    def __init__(self, args, spec, antenna, samp_rate, gain=None, calibration=0.0):
        gr.hier_block2.__init__(
            self, "uhd_src",
            gr.io_signature(0, 0, 0),                    # Input signature
            gr.io_signature(1, 1, gr.sizeof_gr_complex))  # Output signature

        self._src = \
            uhd.usrp_source(device_addr=args,
                            stream_args=uhd.stream_args('fc32'))

        # Set the subdevice spec
        if spec:
            self._src.set_subdev_spec(spec, 0)

        # Set the antenna
        if antenna:
            self._src.set_antenna(antenna, 0)

        self._src.set_samp_rate(samp_rate)
        dev_rate = self._src.get_samp_rate()
        self._samp_rate = samp_rate

        # Resampler to get to exactly samp_rate no matter what dev_rate is
        self._rrate = samp_rate / dev_rate
        self._resamp = filter.pfb.arb_resampler_ccf(self._rrate)

        # If no gain specified, set to midrange
        gain_range = self._src.get_gain_range()
        if gain is None:
            gain = (gain_range.start() + gain_range.stop()) / 2.0
            print("Using gain: ", gain)
        self._src.set_gain(gain)

        self._cal = calibration
        self.connect(self._src, self._resamp, self)

    def tune(self, freq):
        """ Set center frequency """
        self._src.set_center_freq(freq + self._cal, 0)

    def rate(self):
        """ Set sampling rate """
        return self._samp_rate


class app_top_block(gr.top_block):
    def __init__(self, options):
        gr.top_block.__init__(self)
        self.options = options

        (dev_rate, channel_rate, audio_rate, channel_pass, channel_stop, demod) = \
            DEMOD_PARAMS[options.modulation]

        dev = uhd_src(options.args,             # UHD device address
                      options.spec,             # device subdev spec
                      options.antenna,          # device antenna
                      dev_rate,                 # device sample rate
                      options.gain,             # Receiver gain
                      options.calibration)      # Frequency offset
        dev.tune(options.frequency)

        if_rate = dev.rate()
        channel_decim = if_rate // channel_rate
        audio_decim = channel_rate // audio_rate

        chan_taps = filter.optfir.low_pass(1.0,          # Filter gain
                                           if_rate,      # Sample rate
                                           channel_pass,  # One sided modulation bandwidth
                                           channel_stop,  # One sided channel bandwidth
                                           0.1,          # Passband ripple
                                           60)           # Stopband attenuation

        chan = filter.freq_xlating_fir_filter_ccf(
            channel_decim,  # Decimation rate
            chan_taps,     # Filter taps
            0.0,           # Offset frequency
            if_rate)       # Sample rate

        rfsql = analog.pwr_squelch_cc(
            options.rf_squelch,    # Power threshold
            125.0 / channel_rate,    # Time constant
            int(channel_rate / 20),  # 50ms rise/fall
            False)                 # Zero, not gate output

        agc = analog.agc_cc(1.0 / channel_rate,  # Time constant
                            1.0,               # Reference power
                            1.0)               # Gain

        demod = demod(channel_rate, audio_decim)

        # From RF to audio
        #self.connect(dev, chan, rfsql, agc, demod)
        self.connect(dev, chan, demod)

        # Optionally add CTCSS and RSAMP if needed
        tail = demod
        if options.ctcss != None and options.ctcss > 60.0:
            ctcss = analog.ctcss_squelch_ff(audio_rate,    # Sample rate
                                            options.ctcss)  # Squelch tone
            self.connect(demod, ctcss)
            tail = ctcss

        if options.output_rate != audio_rate:
            out_gcd = math.gcd(audio_rate, options.output_rate)
            out_interp = options.output_rate // out_gcd
            out_decim = audio_rate // out_gcd
            rsamp = filter.rational_resampler_fff(out_interp, out_decim)
            self.connect(tail, rsamp)
            tail = rsamp

        # Send to audio output device
        audio_sink = audio.sink(int(options.output_rate),
                                options.audio_output)
        self.connect(tail, audio_sink)

        # Direct asynchronous notifications to callback function
        if self.options.show_async_msg:
            self.async_msgq = gr.msg_queue(0)
            self.async_src = uhd.amsg_source("", self.async_msgq)
            self.async_rcv = uhd.msgq_runner(
                self.async_msgq, self.async_callback)

    def async_callback(self, msg):
        """ Callback for processing async messages """
        md = self.async_src.msg_to_async_metadata_t(msg)
        print("Channel: {} Time: {} Event: {}".format(
            md.channel,
            md.time_spec.get_real_secs(),
            md.event_code))


def main():
    """ Go, Go, Go """
    parser = ArgumentParser()
    parser.add_argument("-a", "--args", default="",
                        help="UHD device address args")
    parser.add_argument("--spec",
                        help="Subdevice of UHD device where appropriate")
    parser.add_argument("-A", "--antenna",
                        help="select Rx Antenna where appropriate")
    parser.add_argument("-f", "--frequency", type=eng_arg.eng_float,
                        default=None, metavar="Hz",
                        help="set receive frequency to Hz")
    parser.add_argument("-c", "--calibration", type=eng_arg.eng_float,
                        default=0.0, metavar="Hz",
                        help="set frequency offset to Hz")
    parser.add_argument("-g", "--gain", type=eng_arg.eng_float,
                        metavar="dB", default=None,
                        help="set RF gain [default is midpoint]")
    parser.add_argument("-m", "--modulation", choices=('AM', 'FM', 'WFM'),
                        metavar="TYPE",
                        help="set modulation type (AM,FM,WFM)")
    parser.add_argument("-o", "--output-rate", type=eng_arg.eng_float,
                        default=32000, metavar="RATE",
                        help="set audio output rate to RATE")
    parser.add_argument("-r", "--rf-squelch", type=eng_arg.eng_float,
                        default=-50.0, metavar="dB",
                        help="set RF squelch to dB [default=%(default)s]")
    parser.add_argument("-p", "--ctcss", type=float,
                        default=None, metavar="FREQ",
                        help="set CTCSS squelch to FREQ")
    parser.add_argument("-O", "--audio-output", default="default",
                        help="pcm device name.  E.g., hw:0,0 or surround51 or /dev/dsp")
    parser.add_argument("--show-async-msg", action="store_true",
                        help="Show asynchronous message notifications from UHD")
    args = parser.parse_args()

    if args.frequency is None:
        sys.stderr.write("Must supply receive frequency with -f.\n")
        sys.exit(1)

    if args.modulation is None:
        sys.stderr.write("Must supply a modulation type (AM, FM, WFM).\n")
        sys.exit(1)

    tb = app_top_block(args)
    try:
        tb.run()
    except KeyboardInterrupt:
        pass


if __name__ == "__main__":
    main()
